package com.hoe.tsb.block;

import com.hoe.tsb.Configuration;
import com.hoe.tsb.fraft.exception.FRaftException;
import com.hoe.tsb.mmk.TraceData;
import com.hoe.tsb.mmk.TraceMmk;
import com.hoe.tsb.tool.SerialUtils;
import com.xiaoleilu.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import org.iq80.leveldb.DB;
import org.iq80.leveldb.Options;
import org.iq80.leveldb.impl.Iq80DBFactory;
import org.springframework.lang.NonNull;
import org.springframework.util.StringUtils;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * <p>
 * 默认的区块链实现
 * </p>
 *
 * @author hoe
 * @version 1.0
 * @date 2022/12/3 13:54
 */
@Slf4j
public class DefaultBlockChain implements BlockChain {

    /**
     * 直接将数据库绑定为LevelDB
     */
    private DB mLevelDb;

    public DefaultBlockChain() {
        this(Configuration.getDefault());
    }

    public DefaultBlockChain(Configuration configuration) {
        Iq80DBFactory factory = Iq80DBFactory.factory;
        Options options = new Options();
        options.createIfMissing(true);
        try {
            mLevelDb = factory.open(new File(configuration.getLevelDbDataPath()), options);
        } catch (IOException e) {
            log.error("初始化LevelDb出错", e);
            throw new FRaftException("初始化LevelDb出错");
        }
    }

    @Override
    public Block gen(List<TraceData> dataList) {
        synchronized (DefaultBlockChain.class) {
            String previousHash;
            long height;

            Block previousBlock = getBlock(LATEST_BLOCK_HASH_KEY);
            //创世区块
            if (previousBlock == null) {
                previousHash = null;
                height = 1;
            } else {
                previousHash = previousBlock.getHash();
                height = previousBlock.getHeight() + 1L;
            }

            Block genBlock = new Block();
            genBlock.setHeight(height);
            genBlock.setPreviousHash(previousHash);
            genBlock.setVersion(VERSION);
            genBlock.setMmk(new TraceMmk(dataList));
            genBlock.setHash(DigestUtil.sha256Hex(SerialUtils.serialize(genBlock)));

            return genBlock;
        }
    }

    @Override
    public void saveBlock(@NonNull Block block) {
        String hash = block.getHash();
        if (StringUtils.isEmpty(hash)) {
            throw new FRaftException("待保存区块不合法，请先计算Hash");
        }

        synchronized (DefaultBlockChain.class) {
            byte[] key = hash.getBytes(StandardCharsets.UTF_8);
            if (mLevelDb.get(key) != null) {
                throw new FRaftException("区块已经保存成功，无需重复提交");
            }
            mLevelDb.put(key, SerialUtils.serialize(block));
            mLevelDb.put(LATEST_BLOCK_HASH_KEY.getBytes(StandardCharsets.UTF_8), SerialUtils.serialize(block));

            //需要往K-V数据库中保存TraceHash-BlockHash的数据，以便构建完整的双链结构，提升溯源数据链的查询性能
            byte[] blockHashBytes = hash.getBytes(StandardCharsets.UTF_8);
            for (TraceData trace : block.getMmk().getLeafDataMap().values()) {
                mLevelDb.put(trace.getTraceHash().getBytes(StandardCharsets.UTF_8), blockHashBytes);
            }
        }
    }

    @Override
    public long getHeight() {
        Block block = getBlock(LATEST_BLOCK_HASH_KEY);
        return block == null ? 0 : block.getHeight();
    }

    @Override
    public Block getBlock(String hash) {
        byte[] blockBytes = mLevelDb.get(hash.getBytes(StandardCharsets.UTF_8));
        if (blockBytes == null) {
            return null;
        }

        return (Block) SerialUtils.deserialize(blockBytes);
    }

    @Override
    public String getBlockHash(String traceHash) {
        byte[] blockHashBytes = mLevelDb.get(traceHash.getBytes(StandardCharsets.UTF_8));
        if (blockHashBytes == null) {
            return null;
        }

        return new String(blockHashBytes, StandardCharsets.UTF_8);
    }

    @Override
    protected void finalize() throws Throwable {
        if (mLevelDb != null) {
            try {
                mLevelDb.close();
            } catch (Exception e) {
                log.error("关闭LevelDB出错", e);
            } finally {
                mLevelDb = null;
            }
        }
        super.finalize();
    }
}
